Os tipos em Julia são organizados em uma árvore. Na raiz da arvore está o tipo Any
e dizemos que Any
é supertipo de qualquer tipo em Julia. De forma equivalente, dizemos que todos os tipos dão subtipos de Any
:
In [1]:
Any
Out[1]:
In [2]:
subtypes(Any)
Out[2]:
Ainda na anologia com uma árvore, cada um de seus nós (incluindo a raiz, Any
) é um tipo abstrato. As folhas da árvore, tipos que não possuem nenhum subtipo, são ditas tipos concretos.
Tipos abstratos não podem ser contruídos, ou seja, nenhum objeto é de um tipo abstrato. Criamos um tipo abstrato fazendo:
In [3]:
abstract Pessoa
Tipos concretos no entanto, não só podem ser construídos, como também possuem campos. Estes campos são onde guardamos os dados do objeto. Isso ficará claro com a definição do tipo concreto Foo
:
In [4]:
type Foo
bar
número::Int
end
Note que esse tipo possui dois campos: o primeiro é bar
e pode ser qualquer tipo de dado; o segundo é número
e deve ser obrigatóriamente do tipo Int
. Mas como construimos um objeto do tipo Foo
? Por padrão, Julia cria uma função com o mesmo nome do tipo, cujos parametros são referentes a cada campo. Exemplo:
In [5]:
a = Foo("Texto", 1234)
println(typeof(a))
Futuramente mostraremos como esse construtor padrão pode ser mudado.
Podemos acessar os campos de a
, fazendo:
In [6]:
a.bar
Out[6]:
In [7]:
a.número
Out[7]:
Para criar um subtipo concreto de Pessoa
, fazemos:
In [8]:
type Brasileiro <: Pessoa
nome::String
RG::Int
vivo::Bool
end
In [9]:
super(Brasileiro) #Supertipo de Brasileiro
Out[9]:
In [10]:
brazuca = Brasileiro("Paulo", 12345, true)
Out[10]:
Seria conveniente criarmos um atalho para construir Brasileiros com o campo vivo == true
. Muito simples, basta fazermos:
In [11]:
Brasileiro(nome, RG) = Brasileiro(nome, RG, true)
Out[11]:
In [12]:
outro_brazuca = Brasileiro("Artur", 12345)
Out[12]:
Nossa intenção nessa seção é criar um tipo que se comporta da seguinte maneira: ao criarmos um objeto Mod2, passamos um número n
, porém o que fica guardado no campo valor
deste objeto será n
módulo 2 (ou seja, n % 2
).
Para fazermos isso, usaremos uma marotagem:
Por padrão temos a função construtora Mod2(x)
que pega o argumento x
e coloca no campo valor
do objeto. Nós trocaremos essa função padrão por uma função contrutora Mod2(x)
que recebe um argumento x
e guarda x % 2
no campo valor
do objeto.
In [13]:
type Mod2 <: Integer
valor::Int
Mod2(valor) = new(valor % 2) #Nova função construtora
end
Observe como criamos novos objetos desse tipo:
In [14]:
Mod2(0)
Out[14]:
In [15]:
Mod2(1)
Out[15]:
In [16]:
Mod2(2)
Out[16]:
In [17]:
Mod2(1123123)
Out[17]:
Mas porque precisamos ficar limitador ao módulo 2? Porque não podemos fazer tipos módulo k, para qualquer k inteiro?
Ao invés de criarmos um tipo para cada k, podemos criar um tipo parametrizado por k. Ou seja, teremos um tipo ModInt{k}
para cada valor de k
.
In [18]:
type ModInt{k} <: Integer
valor::Int
ModInt(valor) = new(valor % k)
end
In [19]:
ModInt{2}(2)
Out[19]:
In [20]:
ModInt{5}(3123)
Out[20]:
Observe que cada ModInt{k}
é um tipo por si mesmo:
In [21]:
v = ModInt{10}(12312)
typeof(v)
Out[21]:
Mas chamar ModInt{k}(valor)
toda vez que quisermos criar um objeto desse tipo é um saco. Por isso, podemos criar um atalho, e fazer um construtor do tipo ModInt(k, valor)
:
In [22]:
ModInt(valor, k) = ModInt{k}(valor)
Out[22]:
In [23]:
ModInt(11,5)
Out[23]:
Mas ainda temos um problema: toda vez que imprimimos o valor de ModInt{k}
, na tela aparece ModInt{k}(valor)
. Convenhamos que esse padrão é muito feio.
Podemos imprimir esse tipo na tela de maneira mais bonita:
In [24]:
importall Base
#Ensina a mostrar ModInt{k} na tela
show{k}(io::IO, n::ModInt{k}) = print(io, "$(n.valor) mod $k")
Out[24]:
Mas os objetos ModInt{k}
ainda não podem ser somados, subtraidos, etc.
In [25]:
ModInt(11,5) #Deve mostrar algo mais bonito agora
Out[25]:
In [26]:
ModInt(11,5) + ModInt(1,5)
Como definimos isso em Julia? Simples, basta definir as funções +
, -
, e *
para esses tipos
In [27]:
#Ensina cada ModInt{k} a somar, subtrair e multiplicar
#Cria cada método para cada valor de k
-{k}(a::ModInt{k}) = ModInt{k}(-a.value)
-{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor - b.valor)
+{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor + b.valor)
*{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor * b.valor)
Out[27]:
Podemos agora fazer as operações:
In [28]:
ModInt(11,5) + ModInt(1,5)
Out[28]:
In [29]:
ModInt(11,5) * ModInt(1,5)
Out[29]:
Porém os objetos do tipo ModInt{K}
ainda não podem somar com outros tipos, por exemplo, Int
s.
Quando somamos, subtraímos, multiplicamos ou dividimos dois subtipos diferentes de Integer
, como ModInt{k}
e Int
, Julia tenta achar um tipo T
em comum, e converter os tipos para esse tipo T
. No nosso caso, o tipo em comum será o próprio ModInt{k}
, portanto apenar o Int
precisa ser convertido:
In [30]:
promote_rule{k}(::Type{ModInt{k}}, ::Type{Int}) = ModInt{k}
Out[30]:
Porém, ainda não podemos somar os tipos, pois Julia não sabe como fazer a conversão de Int
para ModInt{k}
. Para ensinar Julia a fazer essa conversão, basta definir a função convert
:
In [31]:
convert{k}(::Type{ModInt{k}}, i::Int) = ModInt{k}(i)
Out[31]:
Agora Julia sabe para qual tipo em comum deve promover Int
e ModInt{k}
, e como converter Int
para ModInt{k}
. Já temos todos os ingredientes necessários para fazer as operações aritméticas com esses dois tipos:
In [32]:
ModInt(5,2) + 2
Out[32]:
In [33]:
ModInt(5,2) * 2
Out[33]:
É importante notar que essa promoção é feita automaticamente apenas para as operações +
, -
, *
e /
e apenas para números (subtipos de Number
). Para quaisquer outras operações, essa promoção deve ser definida pelo usuário.